﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace DaveSexton.Labs
{
#if SILVERLIGHT
	public sealed class MethodExperiment : IExperiment		// Must be public for UI binding, which uses reflection.
#else
	internal sealed class MethodExperiment : IExperiment
#endif
	{
		#region Public Properties
		public string DisplayName
		{
			get
			{
				return displayName;
			}
		}

		public string Description
		{
			get
			{
				return description;
			}
		}
		#endregion

		#region Private / Protected
		internal const string MainMethodName = "Main";
		internal const string DefaultExperimentName = "Experiment";

		private static readonly string[] suffixes = new[] { "experiment", "experiments" };

		private readonly string displayName, description;
		private readonly MethodInfo method;
		private readonly ILab lab;
		#endregion

		#region Constructors
		private MethodExperiment(MethodInfo method, ILab lab)
		{
			Contract.Requires(lab != null);
			Contract.Requires(method != null);
#if !NETFX_CORE
			Contract.Requires(method.ReturnType == typeof(void) || method.ReturnType.IsAssignableFrom(typeof(Task)));
#endif
			Contract.Requires(method.GetParameters().Length == 0);
			Contract.Requires(!method.IsGenericMethod);

			this.method = method;
			this.lab = lab;

			displayName = Metadata.GetDisplayName(method, DefaultExperimentName, suffixes);
			description = Metadata.GetDescription(method);
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
#if !WINDOWS_PHONE_70
			Contract.Invariant(!string.IsNullOrWhiteSpace(displayName));
#else
			Contract.Invariant(!string.IsNullOrEmpty(displayName));
#endif
			Contract.Invariant(description != null);
			Contract.Invariant(method != null);
			Contract.Invariant(lab != null);
		}

		public static IList<IExperiment> FindAll(ILab lab)
		{
			Contract.Requires(lab != null);
			Contract.Ensures(Contract.Result<IList<IExperiment>>() != null);
			Contract.Ensures(Contract.Result<IList<IExperiment>>().IsReadOnly);

			var labType = lab.GetType();

#if NETFX_CORE
			var methods = labType.GetTypeInfo().DeclaredMethods;
#else
			var methods = labType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#endif

			var list = methods
				.Where(method => MethodExperiment.IsNamedExperiment(method) || MethodExperiment.IsPublicExperiment(method, labType))
				.Select(method => (IExperiment)new MethodExperiment(method, lab))
				.ToList()
				.AsReadOnly();

			Contract.Assume(((IList<IExperiment>)list).IsReadOnly);

			return list;
		}

		private static bool IsNamedExperiment(MethodInfo method)
		{
			Contract.Requires(method != null);

			try
			{
				var attributes = method.GetCustomAttributes(typeof(ExperimentAttribute), inherit: true);

#if NETFX_CORE
				return attributes.Any();
#else
				return attributes.Length > 0;
#endif
			}
			catch (ArgumentNullException)
			{
				// Occurred during testing in WP7.1 for "$InvariantMethod$".  This appears to be a bug in .NET (and perhaps the CC rewriter).
				return false;
			}
		}

		private static bool IsPublicExperiment(MethodInfo method, Type labType)
		{
			Contract.Requires(method != null);
			Contract.Requires(labType != null);

#if NETFX_CORE
			var methodReturnTypeInfo = method.ReturnType.GetTypeInfo();
			var taskTypeInfo = typeof(Task).GetTypeInfo();
#else
			var methodReturnTypeInfo = method.ReturnType;
			var taskTypeInfo = typeof(Task);
#endif

			return method.IsPublic
					&& !string.Equals(method.Name, MainMethodName, StringComparison.Ordinal)	// required to exclude Main method overrides in F#
					&& method.DeclaringType == labType
					&& (method.ReturnType == typeof(void) || methodReturnTypeInfo.IsAssignableFrom(taskTypeInfo))
					&& method.GetParameters().Length == 0
					&& !method.IsGenericMethod;
		}

		public Task ExecuteAsync()
		{
			var task = method.Invoke(lab, null) as Task;

			if (task != null)
			{
				return task;
			}

#if (SILVERLIGHT && !WINDOWS_PHONE) || NET40
			return TaskEx.FromResult(true);
#else
			return Task.FromResult(true);
#endif
		}
		#endregion
	}
}